package com.schuxl.immo.observer.observer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

import javax.mail.internet.InternetAddress;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import org.apache.log4j.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

import com.schuxl.immo.observer.StartImmoObserver;
import com.schuxl.immo.observer.dataobject.Ad;
import com.schuxl.immo.observer.dataobject.WillhabenAd;
import com.schuxl.immo.observer.dataobject.schematypen.filter.House;
import com.schuxl.immo.observer.dataobject.schematypen.filter.Houses;
import com.schuxl.immo.observer.dataobject.schematypen.sites.Sites.Site;
import com.schuxl.immo.observer.mail.MailSender;
import com.schuxl.immo.observer.util.Util;
import com.schuxl.immo.observer.util.maps.DistanceCalculator;
import com.schuxl.immo.observer.util.maps.GeoData;


public abstract class ImmoObserver<T extends Ad> implements Runnable {

	
	private static Logger LOGGER = Logger.getLogger(ImmoObserver.class);
	private static Houses HOUSES = null;
	protected Site site;
	public static final DateFormat DATE_FORMATTER = new SimpleDateFormat("dd.MM.yy 'um' HH:mm:ss");
	
	
	
	static {
		try {
			JAXBContext jaxbContext = JAXBContext.newInstance("com.schuxl.immo.observer.dataobject.schematypen.filter");
			Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
		    HOUSES = (Houses)unmarshaller.unmarshal(new File(StartImmoObserver.PROPERTIES.getProperty("com.schuxl.immo.observer.modelFilterFile")));
		} catch(Exception e) {
			LOGGER.error("Fehler in Klasse ImmoObserver beim Instanzieren des JAXB-Contexts!");
		}
		
	}
	
	
	public ImmoObserver(Site site) {
		this.site=site;
	}
	
	
	/**
	 * Parst die Daten von der Uebersichtsseite. 
	 * @param document Document Object einer Site
	 * @return Die erstellte Objektliste
	 */
	protected abstract List<T> parseOverview(Document document);
	

	
	/**
	 * Parst die Daten von der Detailseite 
	 * @param document Document Object einer Site
	 * @return Die erstellte Objektliste
	 */
	protected abstract Ad parseDetailSite(Document document, Ad ad);
	
	
	
	
	@Override
	public synchronized void run() {
		while(true) {
			try {
				List<T> currentList = new ArrayList<T>();
				for(int x=1; x>0; x++) {
					//Testbedingung
		            if(Util.isGoodString(System.getProperty("com.schuxl.immo.observer.siteCounterForTest"))) {
		            	int siteCounterForTest = Integer.parseInt(System.getProperty("com.schuxl.immo.observer.siteCounterForTest"));
		            	if(siteCounterForTest>0 && x > siteCounterForTest) 
		            		break;
		            }
					
					String concatUrl = site.getUrl()+site.getPageParam()+x;
					LOGGER.debug("Hole und parse Seite " + concatUrl);
					URL url = new URL(concatUrl);
					Document document = Jsoup.parse(url, 1000*20);
					int counter = currentList.size();
					currentList.addAll(parseOverview(document));
					
					//Wenn die Anzahl der Eintraege nicht mehr steigt, dann gibt es diese Seite nicht mehr
					if(counter==currentList.size())
						break;
				}
				
				String tempFileName = StartImmoObserver.PROPERTIES.getProperty("com.schuxl.immo.observer.tempFileFolder")+site.getId()+".ser";
	            LOGGER.debug("Lese fileList aus dem File " + tempFileName);
	            File tempFile = new File(tempFileName);
	            List<T> deltaList = new ArrayList<T>();
	            List<T> fileList = null;
	            
	            //Falls es noch keine fileList gibt, wird alles als Aenderung betrachtet
	    		if(tempFile.exists()==false) {
	    			deltaList = currentList;
	    		} else { //Delta zwischen aktueller Liste und File Liste erstellen
	    			fileList = readFile(tempFileName);
	    			deltaList = calculateDeltaList(currentList, fileList, deltaList);
	    		}
	    		
	    		List<T> detailSiteParameterList = null;
	    		
    			LOGGER.debug("Hole Detailseiten Parameter!");
    			detailSiteParameterList = fetchDetailSiteParameters(deltaList);
    			
	    		LOGGER.debug("Filtern der Anzeigen");
	    		List<T> filteredList = filterRelevantItems(detailSiteParameterList);
	    		
	    		LOGGER.debug("Berechne Google Maps Distanzen");
	    		List<T> travelParameterList = calculateTravelParameters(filteredList);
	    		
	    		LOGGER.debug("Versende Mail.");
	    		buildAndSendMail(travelParameterList);
	    		
	    		LOGGER.debug("Merge currentList und fileList");
	    		List<T> list = mergeAdList(currentList, fileList);
	    		
	    		LOGGER.debug("Bereinige veraltete Elemente");
	    		list = cleanupAdList(list);
	    		
	    		LOGGER.debug("Schreibe Listen in File " + tempFileName);
	    		writeFile(tempFileName, list);
	    	    
		        LOGGER.debug("Thread wird fuer "+site.getIntervallInMinutes()+" Minuten in Schlaf versetzt");
				Thread.sleep(Integer.parseInt(site.getIntervallInMinutes())*60*1000);
			}
			catch(Exception e) {
				LOGGER.error("Exception: ", e);
				try {
					Thread.sleep(10*60*1000); //Sleep 5 minutes
				} catch (InterruptedException e1) {
					LOGGER.error(e);
				} 
			}
		}
	}


	/**
	 * Vergleicht die aktuelle Liste mit der File Liste und erstellt eine Liste mit den unterschiedlichen Anzeigen
	 * @param currentList Aktuell geholte Liste
	 * @param tempFileName Name des Files mit der alten Liste
	 * @param deltaList 
	 * @return
	 * @throws Exception
	 */
	private List<T> calculateDeltaList(List<T> currentList, List<T> fileList, List<T> deltaList) throws Exception {
		LOGGER.debug("Vergleiche Daten von currentList und fileList");
		LOGGER.debug("Anzahl Saetze in currentList: " + currentList.size());
		LOGGER.debug("Anzahl Saetze in fileList: " + fileList.size());
		for(T currentAd : currentList) {
			boolean foundIt = false;
			for(T fileAd: fileList) {
				if(fileAd.equals(currentAd)) {
					foundIt = true;
					break;
				} 
			}
			if(foundIt==false)
				deltaList.add(currentAd);
		}
		return deltaList;
	}
	
	

	


	/**
	 * Gibt alle neuen Ads zu den bisherigen Ads dazu.
	 * @param currentList
	 * @param fileList
	 * @return mergedList
	 */
	private synchronized List<T> mergeAdList(List<T> currentList, List<T> fileList) {
		//Wenn es noch keine fileList gab ist sie NULL und daher ist die currentList die gemergte Liste
		if(fileList==null)
			return currentList;
		
		//Mergen der Listen
		List<T> mergedList = new ArrayList<T>();
		mergedList.addAll(fileList); //Die von der aktuellen Liste kommen schon mal auf jeden Fall rein
		for(T currentAd : currentList) {
	    	boolean foundIt = false;
	    	for(T fileAd: fileList) {
	    		if(fileAd.equals(currentAd)) {
	    			foundIt = true;
	    			break;
	    		} 
	    	}
	    	if(foundIt==false)
	    		mergedList.add(currentAd);
	    }
		return mergedList;
	}
	
	
	
	
	
	/**
	 * Ads die schon aelter als 1 Woche sind, werden ausgemistet
	 * @param list
	 * @return Die bereinigte Liste
	 */
	private synchronized List<T> cleanupAdList(List<T> list) {
		Calendar datumVorSiebenTagen = Calendar.getInstance();
		datumVorSiebenTagen.add(Calendar.DATE, -180);
		
		//Vergabe der Zeitstempel (IssueDate) und ausmisten der alten Ads
		//Dies muss mit einem Iterator geloest werden (nicht foreach), da man aus Listen ueber die man schleift, keine Elemente loeschen kann
		Iterator<T> iterator = list.iterator();
		while (iterator.hasNext()) {
		    T currentAd = iterator.next();
		    
		    //Alle die noch keinen Zeitstempel (IssueDate) haben, bekommen jetzt einen)
			if(currentAd.getIssueDate()==null)
				currentAd.setIssueDate(Calendar.getInstance());
			//All diejenigen deren IssudeDate aelter als 7 Tage ist fallen raus
			if(currentAd.getIssueDate().before(datumVorSiebenTagen))
				iterator.remove();
		}
		return list;
	}


	
	
	
	/**
	 * Filtered aus der deltaList die dem model-filter entsprechenden Bikes heraus.
	 * @param deltaList
	 * @return Dem Filter entsprechende Bikes
	 */
	private synchronized List<T> filterRelevantItems(List<T> deltaList) {
		List<T> filteredList = new ArrayList<T>();

		// Mit der FOREACH Schleife hat er die Schleife leider scheinbar 2x gemacht
		Ad: 
		for (int i = 0; i < deltaList.size(); i++) {
			T currentAd = deltaList.get(i);
			for (House house : HOUSES.getHouse()) {
				//Ist das Haus bereits verkauft?
				if(currentAd.getTitle().toLowerCase().contains("verkauft"))
					continue Ad;
				//Ist das Haus zu teuer?
				if(currentAd.getPrice()>420000)
					continue Ad;
				
				// Filtert auf Basis von Uebersichtsseiten-Patterns
				String[] plzOutArray = house.getPlzOutFilter().split(",");
				for (String plzOut : plzOutArray) {
					if (Util.isGoodString(plzOut) && currentAd.getLocation().contains(plzOut))
						continue Ad;
				}

				//Filtern der Wohnflaeche
				if(currentAd.getLivingSpace()==null || currentAd.getLivingSpace() >= 230)
					continue Ad;
				
				// Filtert auf Basis von Detailseiten-Patterns
				Pattern detailSiteRejectPattern = Pattern.compile(house.getDetailSiteReject(), Pattern.CASE_INSENSITIVE);
				if (Util.isGoodString(house.getDetailSiteReject())==false || detailSiteRejectPattern.matcher(currentAd.getDetailSiteText()).find() == false)
					filteredList.add(currentAd);
			}
		}

		return filteredList;
	}
	
	
	
	
	/**
	 * Zumindest fuer willhaben.at Sites wird die Location aus der Detailseite nachgeladen 
	 * @param filteredList
	 * @return Die Liste der Ads mit der detaillierten Location
	 */
	private List<T> fetchDetailSiteParameters(List<T> filteredList) {
		for(Ad ad : filteredList) {
			if(ad instanceof WillhabenAd) {
				
				try {
					Document document = Jsoup.parse(new URL(ad.getLink()), 1000*15);
					ad = parseDetailSite(document, ad);
				}catch(Exception e) {
					LOGGER.error("Fehler beim Holen der Location aus der Detailseite." + e);
				}
			}
	    }
		return filteredList;
	}
	
	


	/**
	 * Holt fuer jedes Bike die Entfernung von Wien/Dobersberg bis dorthin wo das Bike steht und befuellt die entsprechenenden Properties
	 * Wenn der Ort nicht eindeutig ist oder nicht erkannt wird, dann wird es leer gelassen
	 * @param filteredList
	 * @return Gibt die erweiterte/befuellte Liste zurueck
	 */
	private List<T> calculateTravelParameters(List<T> filteredList) {
		for(int i=0; i<filteredList.size(); i++) {
			Ad ad = filteredList.get(i);
			System.out.println(ad.getLocation());
			GeoData geoDataWienMitte = DistanceCalculator.getGeoData("Wien Mitte", ad.getLocation());
			if(geoDataWienMitte!=null) {
				ad.setDistanceWienMitte(geoDataWienMitte.getDistance());
				ad.setTravelTimeWienMitte(geoDataWienMitte.getDuration());
				ad.setLinkWienMitte(geoDataWienMitte.getLink());
			}
			GeoData geoDataLkwWalter = DistanceCalculator.getGeoData("LKW Walter", ad.getLocation());
			if(geoDataLkwWalter!=null) {
				ad.setDistanceLkwWalter(geoDataLkwWalter.getDistance());
				ad.setTravelTimeLkwWalter(geoDataLkwWalter.getDuration());
				ad.setLinkLkwWalter(geoDataLkwWalter.getLink());
			}
			if(geoDataWienMitte!=null)
				ad.setGeoImageLink(DistanceCalculator.getMapLink(ad.getLocation(), geoDataWienMitte.getLat(), geoDataWienMitte.getLng()));
	    }
		return filteredList;
	}

	
	
	
	/**
	 * Baut den Mailinhalt zusammen u. versendet das Mail
	 * @param List
	 */
	private synchronized void buildAndSendMail(List<T> filteredList) throws Exception {
		LOGGER.debug("Mail Inhalt zusammenbauen.");
		StringBuffer stringBuffer = new StringBuffer();
		stringBuffer.append("<html><head/><body>");
		stringBuffer.append("<hr/>");
		for(T ad : filteredList) {
			stringBuffer.append(ad.getHtmlRepresentation());
			stringBuffer.append("<hr/>");
		}
		stringBuffer.append("</body></html>");
		
		if(filteredList.size()>0) {
			LOGGER.debug("Mail wird an Subscriber gesendet.");
			InternetAddress[] empfaengerListe = new InternetAddress[site.getSubscribers().getSubscriber().size()];
			for(int i=0; i<site.getSubscribers().getSubscriber().size();i++) {
				InternetAddress internetAddress = new InternetAddress(site.getSubscribers().getSubscriber().get(i).getEmail());
				empfaengerListe[i]=internetAddress;
			}
			MailSender.sendMessage(empfaengerListe, site.getId()+" - "+DATE_FORMATTER.format(new Date()), stringBuffer.toString());
		}
	}



	/**
	 * Liest das File ein, dass die Daten der letzten Abfrage enthaelt
	 * @param fileName
	 * @return
	 * @throws Exception
	 */
	protected List<T> readFile(String fileName) throws Exception {
		ObjectInputStream ois = null;
		try {
		    FileInputStream fis = new FileInputStream(fileName);
		    ois = new ObjectInputStream(fis);
		    return (List<T>) ois.readObject();
		} finally {
			ois.close();
		}
	}
	
	
	
	/**
	 * Schreibt das File weg, dass die Daten der aktuellen Abfrage enthaelt
	 * @param fileName
	 * @param currentList
	 * @throws Exception
	 */
	protected void writeFile(String fileName, List<T> currentList) throws Exception {
		ObjectOutputStream oos = null;
		try {
		    FileOutputStream fos = new FileOutputStream(fileName);
		    oos = new ObjectOutputStream(fos);
		    oos.writeObject(currentList);
		} finally {
			oos.flush();
			oos.close();
		}
	}


	
	
	

	
	
	
}
